function set(key, value, obj) {
  const clone = { ...obj };
  clone[key] = value;
  return clone;
}

function generateData(timers) {
  return Object.keys(timers).reduce(
    (acc, cur) =>
      set(
        cur,
        {
          isRunning: false,
          timer: timers[cur].time || 0,
          instance: null,
          tickTimes: 0,
        },
        acc
      ),
    {}
  );
}

function setTimer(repeat) {
  return repeat ? setInterval : setTimeout;
}

function clearTimer(repeat) {
  return repeat ? clearInterval : clearTimeout;
}

function generateTimer(options, vm) {
  return setTimer(options.repeat)(() => {
    vm.timers[options.name].tickTimes++;
    vm.$emit(`timer-tick:${options.name}`);
    options.callback();
  }, options.time);
}

function normalizeConfig(config, vm) {
  if (process.env.NODE_ENV !== 'production') {
    if (!config.name) {
      throw new Error('[vue-timers.create] name is required');
    }
    if (!config.callback && typeof vm[config.name] !== 'function') {
      throw new ReferenceError(`[vue-timers.create] Cannot find method ${config.name}`);
    }
    if (config.callback && typeof config.callback !== 'function') {
      throw new TypeError(`[vue-timers.create] Timer callback should be a function, ${typeof config.callback} given`);
    }
  }
  return {
    repeat: false,
    time: 0,
    immediate: false,
    autostart: false,
    ...config,
    callback: (config.callback && config.callback.bind(vm)) || vm[config.name],
  };
}

function normalizeOptions(options, vm) {
  return Array.isArray(options)
    ? options.reduce((res, config) => set(config.name, normalizeConfig(config, vm), res), {})
    : Object.keys(options).reduce((res, key) => set(key, normalizeConfig(set('name', key, options[key]), vm), res), {});
}

export default {
  data() {
    if (!this.$options.timers) {
      return {};
    }
    this.$options.timers = normalizeOptions(this.$options.timers, this);
    return {
      timers: generateData(this.$options.timers),
    };
  },

  created() {
    if (!this.$options.timers) {
      return;
    }
    const vm = this;
    const data = vm.timers;
    const options = vm.$options.timers;
    vm.$timer = {
      start(name) {
        if (process.env.NODE_ENV !== 'production' && !(name in options)) {
          throw new ReferenceError(`[vue-timers.start] Cannot find timer ${name}`);
        }
        data[name].isRunning = true;
        data[name].instance = generateTimer(set('timer', data[name].time, options[name]), vm);
        if (options[name].immediate) {
          data[name].tickTimes++;
          vm.$emit(`timer-tick:${name}`);
          options[name].callback();
        }
        vm.$emit(`timer-start:${name}`);
      },

      stop(name) {
        if (process.env.NODE_ENV !== 'production' && !(name in options)) {
          throw new ReferenceError(`[vue-timers.stop] Cannot find timer ${name}`);
        }
        if (!data[name].isRunning) {
          return;
        }
        clearTimer(options[name].repeat)(data[name].instance);
        data[name].isRunning = false;
        vm.$emit(`timer-stop:${name}`);
      },
    };
  },

  mounted() {
    if (!this.$options.timers) {
      return;
    }
    const vm = this;
    const options = vm.$options.timers;
    Object.keys(options).forEach(key => {
      if (options[key].autostart) {
        vm.$timer.start(key);
      }
    });
  },

  beforeDestroy() {
    if (!this.$options.timers) {
      return;
    }
    const vm = this;
    Object.keys(vm.$options.timers).forEach(key => {
      vm.$timer.stop(key);
    });
  },
};
